/*
 * @Author: 黄威
 * @Date: 2021-07-28 11:59:07
 * @LastEditors: 黄威
 * @LastEditTime: 2022-03-03 16:27:38
 * @Description: graphic构造工具类
 */
import type {
    BindMap,
    GraphicLayer,
    Graphic,
    GraphicType,
    Style,
    MarkerStyle,
    LabelStyle,
} from 'types'

import L from 'leaflet'
import * as Cesium from 'Cesium'
import * as turf from '@turf/turf'

import { imageSourceManager, setStyle } from './style'
import { toArray, coordsToLLs, coordsToC3s } from '../coordinate'
import { toBoundary } from '../view'

// 获取一起特殊图形的函数
export const ext = {
    getRectangle(
        p1: [number, number],
        p2: [number, number]
    ): [number, number][] {
        const [x1, y1] = p1
        const [x2, y2] = p2

        return [
            [x1, y1],
            [x1, y2],
            [x2, y2],
            [x2, y1],
            [x1, y1],
        ]
    },
}

/* *************** 2维绘制类型 **************** */
export class LGraphicLayer implements GraphicLayer {
    static wrap(layer: any): LGraphicLayer {
        if (layer instanceof LGraphicLayer) return layer

        const ps = Object.create(LGraphicLayer.prototype)
        ps.layer = layer
        ps.constructor = LGraphicLayer

        return ps
    }

    layer?: L.LayerGroup

    constructor(bindMap: BindMap) {
        this.layer = L.layerGroup().addTo(bindMap.map)
    }

    has(g: Graphic): boolean {
        return this.layer!.hasLayer(g.feature)
    }

    add(g: Graphic): void {
        if ((g.feature as any)._mapToAdd) {
            return
        }
        g.feature.addTo(this.layer)
    }

    remove(id: any): void {
        this.layer?.removeLayer(id)
    }

    clear() {
        this.layer!.clearLayers()
    }

    destroy() {
        this.clear()
        this.layer!.remove()
        this.layer = undefined
    }
}

class LGraphic implements Graphic {
    type: GraphicType
    feature: any

    get id() {
        return this.feature._leaflet_id
    }

    constructor(type: GraphicType, Poly: any) {
        this.type = type
        this.feature = new Poly([])
        this.feature._graphic = this
        this.setStyle({})
    }

    setCoords(coords: [number, number][] = []) {
        // 由子类实现
        return this
    }

    setStyle(style: Style | MarkerStyle | LabelStyle) {
        setStyle(this.type, style, this.feature)
        return this
    }

    addTo(target: LGraphicLayer) {
        LGraphicLayer.wrap(target).add(this)
        return this
    }

    remove() {
        this.feature.remove()
        return this
    }
}
class PointGraphic extends LGraphic {
    constructor() {
        super('Point', L.CircleMarker)
    }

    setCoords(coords: any[]) {
        const point = coordsToLLs(toArray('Point', coords) as any)[0]
        this.feature.setLatLng(point)
        return this
    }
}
class PolylineGraphic extends LGraphic {
    constructor() {
        super('Polyline', L.Polyline)
    }

    setCoords(coords: any[]) {
        const polyline = (toArray('Polyline', coords) as any)[0]
        this.feature.setLatLngs(coordsToLLs(polyline))
        return this
    }
}
class PolygonGraphic extends LGraphic {
    constructor() {
        super('Polygon', L.Polygon)
    }

    setCoords(coords: any[]) {
        const polygon = (toArray('Polygon', coords) as any)[0]
        this.feature.setLatLngs(polygon.map(coordsToLLs))
        return this
    }
}
class MarkerGraphic extends LGraphic {
    constructor() {
        super('Marker', L.Marker)
    }
}
class LabelGraphic extends LGraphic {
    constructor() {
        super('Label', L.Marker)
    }
}
MarkerGraphic.prototype.setCoords = LabelGraphic.prototype.setCoords =
    PointGraphic.prototype.setCoords
class RectangleGraphic extends PolygonGraphic {
    constructor() {
        super()
        this.type = 'Rectangle'
    }

    setCoords(coords: any[]) {
        const [p1, p2] = coords
        return super.setCoords(ext.getRectangle(p1, p2))
    }
}
class CircleGraphic extends LGraphic {
    constructor() {
        super('Circle', L.Circle)
    }

    setCoords(coords: any[]) {
        const lls = coordsToLLs(toArray('Point', coords) as any)
        this.feature.setLatLng(lls[0])

        if (lls[1]) {
            this.setRadius(
                turf.distance(coords[0], coords[1], { units: 'meters' })
            )
        }
        return this
    }

    setRadius(radius: number) {
        this.feature.setRadius(radius)
        return this
    }

    getRadius(): number {
        return this.feature.getRadius()
    }
}
class ImageGraphic extends LGraphic {
    constructor() {
        super('Image', L.ImageOverlay)
    }

    setCoords(coords: any[]) {
        const [minx, miny, maxx, maxy] = toBoundary(coords)
        ;(this.feature as L.ImageOverlay).setBounds(
            new L.LatLngBounds([miny, minx], [maxy, maxx])
        )
        return this
    }

    setStyle(style: MarkerStyle) {
        style = Object.assign({ opacity: 1 }, style)

        style.img && (this.feature as L.ImageOverlay).setUrl(style.img)
        ;(this.feature as L.ImageOverlay).setOpacity(style.opacity!)

        return this
    }
}

const GraphicMappers2D: { [key: string]: new (...rest: any[]) => Graphic } = {
    none: LGraphic,
    Point: PointGraphic,
    Polyline: PolylineGraphic,
    Polygon: PolygonGraphic,
    Marker: MarkerGraphic,
    Label: LabelGraphic,
    Rectangle: RectangleGraphic,
    Circle: CircleGraphic,
    Image: ImageGraphic,
}

/* ***************** 3维绘制类型 ***************** */
export class C3GraphicLayer implements GraphicLayer {
    static wrap(layer: any): C3GraphicLayer {
        if (layer instanceof C3GraphicLayer) return layer

        const ps = Object.create(C3GraphicLayer.prototype)
        ps.layer = layer
        ps.constructor = C3GraphicLayer

        return ps
    }

    layer?: Cesium.CustomDataSource

    constructor(bindMap: BindMap) {
        this.layer = new Cesium.CustomDataSource()
        bindMap.map.dataSources.add(this.layer)
        ;(this.layer as any).dataSourceCollection = bindMap.map.dataSources
    }

    has(g: Graphic): boolean {
        return !!this.layer?.entities.contains(g.feature)

        // 已经添加
        // if (

        //     g.feature.entityCollection &&
        //     g.feature.entityCollection.contains(g.feature)
        // ) {
        //     return true
        // }
        // return false
    }

    add(g: Graphic): void {
        // 已经添加
        if (this.has(g)) {
            return
        }

        this.layer!.entities.add(g.feature)
    }

    remove(id: any): void {
        this.layer?.entities.removeById(id)
    }

    clear() {
        this.layer!.entities.removeAll()
    }

    destroy() {
        this.clear()

        const layer: any = this.layer
        if (layer && layer.dataSourceCollection) {
            layer.dataSourceCollection.remove(layer, true)
            delete layer.dataSourceCollection
        }

        this.layer = undefined
    }
}

class C3Graphic implements Graphic {
    type: GraphicType
    feature: Cesium.Entity
    // 动态跟新坐标点（三维更新）
    dynamic: boolean

    get id() {
        return this.feature.id
    }

    constructor(type: GraphicType, dynamic = false) {
        this.type = type
        this.dynamic = dynamic
        this.feature = new Cesium.Entity()
        ;(this.feature as any)._graphic = this
    }

    setCoords(coords: [number, number][] = []) {
        return this
    }

    setStyle(style: Style | MarkerStyle | LabelStyle) {
        setStyle(this.type, style, this.feature)
        return this
    }

    addTo(target: C3GraphicLayer) {
        C3GraphicLayer.wrap(target).add(this)
        return this
    }

    remove() {
        if (
            this.feature.entityCollection &&
            this.feature.entityCollection.contains(this.feature)
        ) {
            this.feature.entityCollection.remove(this.feature)
        }
        return this
    }
}
class C3PointGraphic extends C3Graphic {
    constructor(dynamic: boolean) {
        super('Point', dynamic)

        this.feature.point = new Cesium.PointGraphics({
            heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
        })
        this.setStyle({})
    }

    setCoords(coords: any[]) {
        const point = coordsToC3s(toArray('Point', coords) as any)[0]
        this.feature.position = point
        return this
    }
}
class C3PolylineGraphic extends C3Graphic {
    constructor(dynamic: boolean) {
        super('Polyline', dynamic)

        this.feature.polyline = new Cesium.PolylineGraphics({
            clampToGround: true,
        })
        this.setStyle({})
    }

    setCoords(coords: any[]) {
        const polyline = (toArray('Polyline', coords) as any)[0]

        this.feature.polyline.positions = new Cesium.CallbackProperty(
            () => coordsToC3s(polyline),
            !this.dynamic
        )

        return this
    }
}
class C3PolygonGraphic extends C3Graphic {
    constructor(dynamic: boolean) {
        super('Polygon', dynamic)

        this.feature.polyline = new Cesium.PolylineGraphics({
            clampToGround: true,
        })
        this.feature.polygon = new Cesium.PolygonGraphics({
            heightReference: new Cesium.ConstantProperty(
                Cesium.HeightReference.CLAMP_TO_GROUND
            ),
        })
        this.setStyle({})
    }

    setCoords(coords: any[]) {
        const [position, ...holes] = (toArray('Polygon', coords) as any)[0].map(
            coordsToC3s
        )

        // 补全多边形
        if (
            position.length >= 3 &&
            !position[0].equals(position[position.length - 1])
        ) {
            position.push(position[0])
        }

        this.feature.polyline.positions = new Cesium.CallbackProperty(
            () => position,
            !this.dynamic
        )
        this.feature.polygon.hierarchy = new Cesium.CallbackProperty(
            () =>
                new Cesium.PolygonHierarchy(
                    position,
                    holes.map((hole: any) => new Cesium.PolygonHierarchy(hole))
                ),
            !this.dynamic
        )

        return this
    }
}
class C3MarkerGraphic extends C3Graphic {
    constructor(dynamic: boolean) {
        super('Marker', dynamic)

        this.feature.billboard = new Cesium.BillboardGraphics({
            heightReference: new Cesium.ConstantProperty(
                Cesium.HeightReference.CLAMP_TO_GROUND
            ),
        })
        this.setStyle({})
    }
}
class C3LabelGraphic extends C3Graphic {
    constructor(dynamic: boolean) {
        super('Label', dynamic)

        this.feature.label = new Cesium.LabelGraphics({
            heightReference: new Cesium.ConstantProperty(
                Cesium.HeightReference.CLAMP_TO_GROUND
            ),
        })
        this.setStyle({})
    }
}
class C3ModelGraphic extends C3Graphic {
    constructor(opt_options: any) {
        super('Model', false)

        this.feature.model = new Cesium.ModelGraphics({
            heightReference: new Cesium.ConstantProperty(
                Cesium.HeightReference.CLAMP_TO_GROUND
            ),
            ...opt_options,
        })
        this.setStyle({})
    }

    setStyle(style: any) {
        Object.assign(this.feature.model, style)
        return this
    }
}
C3MarkerGraphic.prototype.setCoords =
    C3LabelGraphic.prototype.setCoords =
    C3ModelGraphic.prototype.setCoords =
        C3PointGraphic.prototype.setCoords
class C3RectangleGraphic extends C3PolygonGraphic {
    constructor(dynamic: boolean) {
        super(dynamic)
        this.type = 'Rectangle'
    }

    setCoords(coords: any[]) {
        const [p1, p2] = coords
        return super.setCoords(ext.getRectangle(p1, p2))
    }
}
class C3CircleGraphic extends C3Graphic {
    constructor(dynamic: boolean) {
        super('Circle', dynamic)
        this.feature.ellipse = new Cesium.EllipseGraphics({
            height: new Cesium.ConstantProperty(0),
        })
    }

    setCoords(coords: any[]) {
        const points = coordsToC3s(toArray('Point', coords) as any)

        this.feature.position = points[0]

        // 设置半斤
        if (points[1]) {
            this.setRadius(
                turf.distance(coords[0], coords[1], { units: 'meters' })
            )
        }
        return this
    }

    setRadius(radius: number) {
        const property = new Cesium.CallbackProperty(
            () => radius,
            !this.dynamic
        )
        this.feature.ellipse.semiMajorAxis = property
        this.feature.ellipse.semiMinorAxis = property
        return this
    }

    getRadius(): number {
        return this.feature.ellipse.semiMajorAxis.getValue()
    }
}
class C3ImageGraphic extends C3Graphic {
    constructor(dynamic: boolean) {
        super('Image', dynamic)
        this.feature.polygon = new Cesium.PolygonGraphics({
            heightReference: new Cesium.ConstantProperty(
                Cesium.HeightReference.CLAMP_TO_GROUND
            ),
        })
        this.setStyle({})
    }

    setCoords(coords: any[]) {
        const [minx, miny, maxx, maxy] = toBoundary(coords)
        coords = ext.getRectangle([minx, miny], [maxx, maxy])

        const [position, ...holes] = (toArray('Polygon', coords) as any)[0].map(
            coordsToC3s
        )

        this.feature.polygon.hierarchy = new Cesium.CallbackProperty(
            () =>
                new Cesium.PolygonHierarchy(
                    position,
                    holes.map((hole: any) => new Cesium.PolygonHierarchy(hole))
                ),
            !this.dynamic
        )

        return this
    }

    setStyle(style: MarkerStyle) {
        style = Object.assign({ opacity: 1 }, style)
        if (style.img) {
            imageSourceManager.load(style.img!, image => {
                this.feature.polygon.fill = true
                this.feature.polygon.material =
                    new Cesium.ImageMaterialProperty({
                        image: new Cesium.ConstantProperty(image),
                        color: new Cesium.ConstantProperty(
                            Cesium.Color.WHITE.withAlpha(style.opacity!)
                        ),
                        transparent: new Cesium.ConstantProperty(true),
                    })
            })
        } else {
            this.feature.polygon.fill = false
        }
        return this
    }
}

const GraphicMappers3D: { [key: string]: new (...rest: any[]) => Graphic } = {
    none: C3Graphic,
    Point: C3PointGraphic,
    Polyline: C3PolylineGraphic,
    Polygon: C3PolygonGraphic,
    Marker: C3MarkerGraphic,
    Label: C3LabelGraphic,
    Model: C3ModelGraphic,
    Rectangle: C3RectangleGraphic,
    Circle: C3CircleGraphic,
    Image: C3ImageGraphic,
}

/* ***************** Graphic工厂 ***************** */
export default function newGraphic(
    type: GraphicType,
    is3d: boolean = false,
    opt_options?: any
) {
    const mappers = is3d ? GraphicMappers3D : GraphicMappers2D

    if (mappers[type] && (type as string) !== 'none') {
        return new mappers[type](opt_options)
    } else {
        return new mappers.none(type, opt_options)
    }
}
